第一步笔记
涵盖章节:Introduction + Setting Up NextJS, Neon PostgreSQL & Drizzle ORM + Pages & Layouts + Groups
关键概念:课程目标是为 React 开发者提供 Next.js 15 App Router 的完整视觉化指导,避免纯文档式学习。构建一个功能完整的博客应用,包括用户认证、帖子 CRUD、点赞、评论、SEO 优化等。
核心技术栈:Next.js 15 (App Router)、Server Components 默认、Server Actions、Drizzle ORM + Neon PostgreSQL、BetterAuth 认证、Tailwind CSS、TypeScript。
教学方式:每个章节对应 GitHub repo 的一个分支,逐步迭代添加功能。
代码片段:本章无具体代码,主要为课程概述。
最佳实践:
关键概念:从零初始化 Next.js 项目,接入 Neon serverless PostgreSQL(免费层),用 Drizzle ORM 做类型安全的数据库交互。包括项目创建、依赖安装、环境变量、schema 定义、迁移生成/推送、seed 测试数据。
核心技术点:
重要代码片段:
项目初始化命令(视频演示):
1npx create-next-app@latest blog-app \2 --ts --eslint --tailwind --app --src-dir --turbo依赖安装:
xxxxxxxxxx21npm install drizzle-orm @neondatabase/serverless2npm install -D drizzle-kit.env.local(Neon 连接字符串):
xxxxxxxxxx11DATABASE_URL=postgresql://user:password@ep-cool-name-123456.us-east-2.aws.neon.tech/dbname?sslmode=require
drizzle.config.ts:
x1import { defineConfig } from 'drizzle-kit';2import { config } from 'dotenv';3config({ path: '.env.local' });45export default defineConfig({6 schema: './src/db/schema.ts',7 out: './drizzle/migrations',8 dialect: 'postgresql',9 dbCredentials: {10 url: process.env.DATABASE_URL!,11 },12});schema.ts(核心表定义,包含关系):
xxxxxxxxxx241import { pgTable, uuid, text, timestamp } from 'drizzle-orm/pg-core';2import { relations } from 'drizzle-orm';34export const users = pgTable('users', {5 id: uuid('id').primaryKey().defaultRandom(),6 name: text('name').notNull(),7 email: text('email').notNull().unique(),8 createdAt: timestamp('created_at').defaultNow().notNull(),9});1011export const posts = pgTable('posts', {12 id: uuid('id').primaryKey().defaultRandom(),13 title: text('title').notNull(),14 content: text('content').notNull(),15 authorId: uuid('author_id').references(() => users.id).notNull(),16 createdAt: timestamp('created_at').defaultNow().notNull(),17});1819export const postRelations = relations(posts, ({ one }) => ({20 author: one(users, {21 fields: [posts.authorId],22 references: [users.id],23 }),24}));package.json 脚本:
xxxxxxxxxx61"scripts": {2 "db:generate": "drizzle-kit generate",3 "db:migrate": "drizzle-kit push",4 "db:studio": "drizzle-kit studio",5 "db:seed": "ts-node src/db/seed.ts"6}seed.ts 示例(简化):
xxxxxxxxxx101import { db } from '@/db';2import { users, posts } from '@/db/schema';3import { faker } from '@faker-js/faker';45await db.insert(users).values(6 Array.from({ length: 10 }, () => ({7 name: faker.person.fullName(),8 email: faker.internet.email(),9 }))10);最佳实践:
迁移文件 commit 到 git(版本控制)。
只在开发环境跑 seed(避免生产污染)。
用 Drizzle Studio (npm run db:studio) 可视化检查数据。
常见坑点:
DATABASE_URL 格式错 → 连接失败(记得加 ?sslmode=require)。
忘记 drizzle-kit 作为 dev 依赖 → generate 命令报错。
生产环境直接 push → 数据丢失风险(建议 generate + review SQL)。
Takeaway:这一步是全课程基础,跑通 DB + 迁移 + seed 后,后续章节才能顺畅查询/操作数据。
关键概念:App Router 文件系统路由:page.tsx 定义页面,layout.tsx 定义共享布局(嵌套支持)。Server Components 默认运行在服务器,无需 "use client"。根布局必须包含 和
。重要代码片段:
app/layout.tsx(根布局):
xxxxxxxxxx241import type { Metadata } from 'next';2import './globals.css';34export const metadata: Metadata = {5 title: 'My Blog App',6 description: 'Next.js 15 Full Course Blog',7};89export default function RootLayout({10 children,11}: Readonly<{12 children: React.ReactNode;13}>) {14 return (15 <html lang="en">16 <body className="antialiased bg-gray-50">17 <header className="p-4 bg-blue-600 text-white">18 <h1>Blog App</h1>19 </header>20 <main className="container mx-auto p-4">{children}</main>21 </body>22 </html>23 );24}app/dashboard/layout.tsx(嵌套布局):
xxxxxxxxxx121export default function DashboardLayout({2 children,3}: {4 children: React.ReactNode;5}) {6 return (7 <div className="bg-green-50 p-6 rounded-lg">8 <h2 className="text-2xl mb-4">Dashboard</h2>9 {children}10 </div>11 );12}app/page.tsx(首页):
xxxxxxxxxx121import Link from 'next/link';23export default function Home() {4 return (5 <div>6 <h1>Welcome to Blog</h1>7 <Link href="/dashboard" className="text-blue-600 hover:underline">8 Go to Dashboard9 </Link>10 </div>11 );12}最佳实践:
根布局放全局样式/Metadata。
子布局放特定 UI(如侧边栏)。
始终用 而非 (支持预取、客户端导航)。
常见坑点:
布局中放 "use client" → 整个子树强制客户端渲染。
忘记 export default → 页面空白。
Takeaway:布局是 Next.js UI 复用核心,理解嵌套机制后,能轻松构建一致的页面结构。
关键概念:使用括号 (group) 文件夹组织路由组,不影响 URL 但允许每个组有独立 layout.tsx。常用于分离公共页面(如首页、博客)和私有页面(如 dashboard)。
核心技术点:
重要代码片段:
目录结构示例:
xxxxxxxxxx101app/2├── (marketing)/3│ ├── layout.tsx // 公共布局:首页、博客4│ ├── page.tsx // URL: /5│ └── blog/6│ └── page.tsx // URL: /blog7├── (app)/8│ ├── layout.tsx // 私有布局:dashboard 等9│ └── dashboard/10│ └── page.tsx // URL: /dashboard
(marketing)/layout.tsx:
xxxxxxxxxx151export default function MarketingLayout({2 children,3}: {4 children: React.ReactNode;5}) {6 return (7 <div className="bg-white">8 <nav className="p-4 bg-gray-800 text-white flex gap-4">9 <Link href="/">Home</Link>10 <Link href="/blog">Blog</Link>11 </nav>12 <main className="p-6">{children}</main>13 </div>14 );15}(app)/layout.tsx(私有):
xxxxxxxxxx81export default function AppLayout({ children }: { children: React.ReactNode }) {2 return (3 <div className="bg-gray-100">4 <header className="p-4 bg-purple-600 text-white">Private App</header>5 {children}6 </div>7 );8}最佳实践:
(marketing) 用于公共/SEO 页面。
(app) 用于需要认证的页面。
每个组可有独立 nav/footer。
常见坑点:
组间导航 → 全页重载(因为 layout 不同)。
忘记布局 → 页面样式混乱。
Takeaway:Route Groups 是组织复杂路由 + 独立布局的最佳方式,避免根布局过度复杂。
这一步结束!如果你准备好,请回复“第二步”或“第2步”,我继续给你下一组章节的详细笔记~ 🚀
第二步笔记
涵盖章节:Dynamic Route Segments + Parallel Routing + Intercepting Routes
关键概念:动态路由通过文件夹名中的方括号 [param] 定义,支持单段、多段、catch-all 和可选 catch-all。params 对象在 Server Components 中是 Promise 类型,必须 await 获取值。动态路由默认动态渲染(force-dynamic),但可通过 generateStaticParams 静态生成。
核心技术点:
重要代码片段:
app/posts/[id]/page.tsx(单段动态):
xxxxxxxxxx291import { notFound } from 'next/navigation';23type Params = Promise<{ id: string }>;45export default async function PostPage({ params }: { params: Params }) {6 const { id } = await params; // 必须 await78 // 模拟 DB 查询9 const post = await db.query.posts.findFirst({10 where: eq(posts.id, id),11 with: { author: true },12 });1314 if (!post) notFound();1516 return (17 <article>18 <h1 className="text-3xl font-bold">{post.title}</h1>19 <p className="text-gray-600">By {post.author.name}</p>20 <div className="mt-6 prose">{post.content}</div>21 </article>22 );23}2425// 静态生成(可选,提升性能)26export async function generateStaticParams() {27 const allPosts = await db.select({ id: posts.id }).from(posts);28 return allPosts.map((p) => ({ id: p.id }));29}多段示例:app/blog/[category]/[slug]/page.tsx → params: { category: string, slug: string }
Catch-all:app/docs/[...path]/page.tsx → params: { path: string[] }
最佳实践:
关键概念:使用 @slotName 语法在同一布局中渲染多个独立路由(并行)。每个 slot 有自己的 page.tsx 和 default.tsx。客户端导航只更新匹配的 slot,不会全重载。常用于 dashboard、多面板布局。
核心技术点:
重要代码片段:
app/dashboard/layout.tsx(主布局):
xxxxxxxxxx191import type { ReactNode } from 'react';23export default function DashboardLayout({4 children, // 默认 slot (app/dashboard/page.tsx)5 analytics, // @analytics6 users, // @users7}: {8 children: ReactNode;9 analytics: ReactNode;10 users: ReactNode;11}) {12 return (13 <div className="grid grid-cols-1 md:grid-cols-3 gap-6 p-6">14 <aside className="bg-white p-4 rounded shadow">{users}</aside>15 <main className="col-span-2 bg-white p-6 rounded shadow">{children}</main>16 <section className="bg-white p-4 rounded shadow">{analytics}</section>17 </div>18 );19}app/dashboard/@analytics/page.tsx(独立 slot 内容):
xxxxxxxxxx91export default function AnalyticsSlot() {2 return (3 <div>4 <h3 className="text-lg font-semibold">Analytics Overview</h3>5 <p>Views: 12k | Likes: 4.2k</p>6 {/* 更多统计 */}7 </div>8 );9}app/dashboard/@analytics/default.tsx(fallback):
xxxxxxxxxx31export default function AnalyticsDefault() {2 return <div className="p-4 text-gray-500">No analytics available</div>;3}最佳实践:
每个 slot 提供 default.tsx 防止 404。
用并行加载慢 slot(Suspense 包裹)。
适合 dashboard、admin 面板、多标签 UI。
常见坑点:
忘记 default.tsx → slot 访问 404。
slot 中放 "use client" → 整个 slot 客户端渲染。
布局 props 顺序错 → UI 错位。
Takeaway:并行路由是复杂布局(如 dashboard)的模块化神器,支持独立加载/更新 slot,提升并发性能。
关键概念:拦截路由在不改变 URL 的情况下渲染特定页面(如模态框),常用于登录/注册弹窗。使用 (.)、(..)、(/) 等相对路径语法。仅在客户端导航(Link、router.push)触发拦截;直接访问仍正常渲染。
核心技术点:
重要代码片段:
目录结构(拦截登录模态):
xxxxxxxxxx51app/2├── login/ // 正常 /login 页面3│ └── page.tsx4└── (.)login/ // 拦截模态5└── page.tsx // 在任何页面导航到 /login 时渲染模态
app/(.)login/page.tsx(模态实现):
xxxxxxxxxx331'use client';23import { useRouter } from 'next/navigation';4import { useEffect } from 'react';56export default function LoginIntercept() {7 const router = useRouter();89 // 按 Esc 关闭10 useEffect(() => {11 const handleEsc = (e: KeyboardEvent) => {12 if (e.key === 'Escape') router.back();13 };14 window.addEventListener('keydown', handleEsc);15 return () => window.removeEventListener('keydown', handleEsc);16 }, [router]);1718 return (19 <div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50">20 <div className="bg-white rounded-lg p-8 max-w-md w-full relative">21 <button22 onClick={() => router.back()}23 className="absolute top-4 right-4 text-gray-500 hover:text-gray-800"24 >25 ×26 </button>27 <h2 className="text-2xl font-bold mb-6">Login</h2>28 {/* 登录表单 */}29 <form>{/* ... */}</form>30 </div>31 </div>32 );33}使用场景:在任何页面 Login → 弹出模态,而不是跳转。
最佳实践:
模态 + URL 支持(分享/后退)。
用 "use client" + useRouter 处理交互。
结合 auth 检查(未登录时拦截)。
常见坑点:
不加 "use client" → 无 router/back 功能。
直接访问 /login → 正常页面而非模态(预期行为)。
拦截语法错 → 不触发。
Takeaway:拦截路由完美实现“软导航 + 模态”体验,常见于登录、编辑、预览等场景,提升交互流畅度。
这一步结束!第二步包含 3 个章节,重点是动态路由、并行和拦截——都是 App Router 的高级路由技巧。
准备好下一组了吗?回复“第三步”或“第3步”,我继续~ 🚀
第三步笔记
涵盖章节:Static Rendering and Caching + Dynamic Rendering and More on Caching + Fetching Data from the Database (Blog Page) + Fetching Data (Single Blog Page)
关键概念:Next.js 15 默认采用静态渲染(Static Rendering / SSG + ISR),在 build 时预生成 HTML + RSC Payload,极大提升首屏速度、SEO 和 CDN 缓存能力。静态路由在生产环境无限期缓存,除非手动 revalidate。
核心技术点:
重要代码片段:
静态路由(默认):
xxxxxxxxxx151// app/about/page.tsx2export const revalidate = 3600; // 1 小时 ISR(可选)34export default async function AboutPage() {5 const data = await fetch('https://api.example.com/about', {6 cache: 'force-cache', // 显式静态缓存(默认)7 }).then(res => res.json());89 return (10 <div>11 <h1>About Us</h1>12 <p>{data.description}</p>13 </div>14 );15}on-demand revalidate(Server Action 或 Route Handler 中):
xxxxxxxxxx91// actions.ts2'use server';3import { revalidatePath } from 'next/cache';45export async function updatePost() {6 // 更新数据库...7 revalidatePath('/blog'); // 失效 /blog 所有页面缓存8 revalidateTag('posts'); // 失效所有带 tag 'posts' 的 fetch9}fetch 缓存控制:
xxxxxxxxxx11fetch(url, { next: { tags: ['posts'], revalidate: 60 } }); // tag + 时间最佳实践:
关键概念:当路由需要实时数据或用户特定内容时,使用动态渲染(Dynamic Rendering),每请求重新执行组件。动态路由自动 force-dynamic,但可混合静态 fetch。
核心技术点:
重要代码片段:
强制动态路由:
xxxxxxxxxx111// app/profile/page.tsx2import { cookies } from 'next/headers';34export const dynamic = 'force-dynamic'; // 或自动触发(cookies())56export default async function ProfilePage() {7 const cookieStore = cookies();8 const theme = cookieStore.get('theme')?.value || 'light';910 return <div>Current theme: {theme}</div>;11}动态 fetch(不缓存):
xxxxxxxxxx31const data = await fetch('https://api.example.com/user', {2 cache: 'no-store', // 每次请求新鲜3}).then(res => res.json());标签缓存 + 失效:
xxxxxxxxxx51// fetch with tag2fetch(url, { next: { tags: ['user-posts'] } });34// 在 Action 中失效5revalidateTag('user-posts');最佳实践:
关键概念:Server Components 直接在组件中使用 Drizzle 查询数据(无需 Route Handler)。列表页查询所有帖子 + 作者 + 点赞数,支持分页、排序。
核心技术点:
重要代码片段:
data.ts(推荐抽象):
xxxxxxxxxx221// src/data/posts.ts2import { db } from '@/db';3import { posts, users, postLikes } from '@/db/schema';4import { desc, count, eq } from 'drizzle-orm';56export async function getPosts(page = 1, limit = 10) {7 const offset = (page - 1) * limit;8 return db.query.posts.findMany({9 with: {10 author: { columns: { id: true, name: true } },11 likes: true,12 },13 orderBy: desc(posts.createdAt),14 limit,15 offset,16 });17}1819export async function getPostCount() {20 const [{ count: total }] = await db.select({ count: count() }).from(posts);21 return total;22}app/blog/page.tsx:
xxxxxxxxxx301import { getPosts, getPostCount } from '@/data/posts';23export default async function BlogPage({ searchParams }: { searchParams: Promise<{ page?: string }> }) {4 const { page = '1' } = await searchParams;5 const currentPage = Number(page);67 const [posts, totalPosts] = await Promise.all([8 getPosts(currentPage),9 getPostCount(),10 ]);1112 const totalPages = Math.ceil(totalPosts / 10);1314 return (15 <div>16 <h1 className="text-4xl font-bold mb-8">Blog Posts</h1>17 <div className="grid gap-6">18 {posts.map(post => (19 <article key={post.id} className="p-6 bg-white rounded shadow">20 <h2 className="text-2xl font-semibold">{post.title}</h2>21 <p className="text-gray-600">By {post.author.name}</p>22 <p className="mt-4">{post.content.substring(0, 150)}...</p>23 <p>Likes: {post.likes.length}</p>24 </article>25 ))}26 </div>27 {/* 分页组件 */}28 </div>29 );30}最佳实践:
查询抽象到 data.ts 文件,便于复用/测试。
Promise.all 并行加载 count 和列表。
Suspense + loading.tsx 提升体验。
常见坑点:
直接在 page.tsx 写查询 → 代码混乱、难维护。
忘记 with: { author: true } → author 为 null。
Takeaway:Server Component 直接 DB 查询是 Next.js 15 最强大特性之一,安全 + 高效。
关键概念:动态路由单条查询 + generateStaticParams 静态生成 + notFound 处理。详情页支持预渲染 known IDs,提升速度。
核心技术点:
重要代码片段:
app/posts/[id]/page.tsx:
xxxxxxxxxx291import { notFound } from 'next/navigation';2import { getPostById } from '@/data/posts';34export async function generateStaticParams() {5 const allPosts = await db.select({ id: posts.id }).from(posts);6 return allPosts.map(p => ({ id: p.id }));7}89// 可选:只允许静态路径10// export const dynamicParams = false;1112export default async function PostPage({ params }: { params: Promise<{ id: string }> }) {13 const { id } = await params;1415 const post = await getPostById(id); // 封装查询1617 if (!post) notFound();1819 return (20 <article className="prose lg:prose-xl mx-auto">21 <h1 className="text-4xl font-bold">{post.title}</h1>22 <p className="text-gray-600 mt-2">23 By {post.author.name} • {post.createdAt.toLocaleDateString()}24 </p>25 <div className="mt-8" dangerouslySetInnerHTML={{ __html: post.content }} />26 <p className="mt-8 text-gray-600">Likes: {post.likes.length}</p>27 </article>28 );29}getPostById(data.ts):
xxxxxxxxxx91export async function getPostById(id: string) {2 return db.query.posts.findFirst({3 where: eq(posts.id, id),4 with: {5 author: { columns: { id: true, name: true } },6 likes: true,7 },8 });9}最佳实践:
generateStaticParams + dynamicParams = false → 完全静态详情页。
notFound() 处理无效 ID。
常见坑点:
generateStaticParams 空 → 所有详情 404。
ID 类型不匹配(string vs uuid)→ 查询失败。
Takeaway:单条数据页结合静态生成实现极致性能 + SEO。
这一步结束!第三步重点是渲染模式、缓存策略和数据获取基础——Next.js 性能优化的核心。
准备好下一组了吗?回复“第四步”或“第4步”继续~ 🚀
第四步笔记
涵盖章节:Streaming + Partial Pre-rendering (PPR) + Route Handlers Overview + Route Handlers Examples (Querying Database)
关键概念:Streaming 是 Next.js 15 核心特性之一,通过 Suspense 和 loading.tsx 实现渐进式渲染:先发送静态/可立即渲染的部分 HTML,再流式发送动态/慢加载内容。极大提升感知加载速度(TTFB)和用户体验,尤其在慢 DB 查询或网络条件下。
核心技术点:
重要代码片段:
app/posts/[id]/loading.tsx(页面级骨架):
xxxxxxxxxx121export default function Loading() {2 return (3 <div className="max-w-3xl mx-auto p-6">4 <div className="h-12 w-3/4 bg-gray-200 rounded animate-pulse mb-4"></div>5 <div className="h-6 w-1/2 bg-gray-200 rounded animate-pulse mb-8"></div>6 <div className="space-y-4">7 <div className="h-32 bg-gray-200 rounded animate-pulse"></div>8 <div className="h-32 bg-gray-200 rounded animate-pulse"></div>9 </div>10 </div>11 );12}颗粒化 Suspense(推荐方式):
xxxxxxxxxx231// app/posts/[id]/page.tsx2import { Suspense } from 'react';3import PostContent from '@/components/PostContent';4import Comments from '@/components/Comments';5import CommentsSkeleton from '@/components/CommentsSkeleton';67export default async function PostPage({ params }: { params: Promise<{ id: string }> }) {8 const { id } = await params;910 return (11 <div className="max-w-3xl mx-auto p-6">12 {/* 静态/快速部分立即渲染 */}13 <Suspense fallback={<div className="h-12 bg-gray-200 rounded animate-pulse" />}>14 <PostContent postId={id} /> {/* 假设快速查询 */}15 </Suspense>1617 {/* 慢加载部分:流式 */}18 <Suspense fallback={<CommentsSkeleton />}>19 <Comments postId={id} /> {/* 假设评论查询慢 */}20 </Suspense>21 </div>22 );23}Comments 组件示例(async Server Component):
xxxxxxxxxx181export default async function Comments({ postId }: { postId: string }) {2 const comments = await db.query.comments.findMany({3 where: eq(comments.postId, postId),4 orderBy: desc(comments.createdAt),5 });67 return (8 <section className="mt-12">9 <h3 className="text-xl font-bold mb-6">Comments ({comments.length})</h3>10 {comments.map(c => (11 <div key={c.id} className="border-b py-4">12 <p>{c.content}</p>13 <p className="text-sm text-gray-500">{c.createdAt.toLocaleString()}</p>14 </div>15 ))}16 </section>17 );18}最佳实践:
关键概念:Next.js 15 实验性特性(Partial Prerendering),在 build 时生成静态“壳”(HTML + 静态部分),运行时填充动态“孔”(Suspense 包裹的动态组件)。结合静态渲染速度 + 动态实时性,显著提升 TTFB 和首屏体验。
核心技术点:
experimental.ppr: 'incremental'(增量模式)。 重要代码片段:
next.config.ts / next.config.js(启用 PPR):
xxxxxxxxxx81/** @type {import('next').NextConfig} */2const nextConfig = {3 experimental: {4 ppr: 'incremental', // 或 true(全量)5 },6};78export default nextConfig;标记动态孔(app/page.tsx 示例):
xxxxxxxxxx231import { connection } from 'next/server'; // 或 cookies()/headers()23export default async function HomePage() {4 // 静态部分立即可用5 const staticData = await fetch('https://api.static.com/data', { cache: 'force-cache' });67 // 标记动态孔8 await connection(); // 或 cookies() → 使以下部分动态910 const dynamicData = await fetch('https://api.dynamic.com/user-data');1112 return (13 <div>14 {/* 静态壳 */}15 <h1>Welcome to Blog</h1>1617 {/* 动态孔 */}18 <Suspense fallback={<UserSkeleton />}>19 <UserProfile data={dynamicData} />20 </Suspense>21 </div>22 );23}最佳实践:
公共/SEO 部分静态,个性化/实时部分动态。
结合 Streaming:动态孔用 Suspense + 骨架。
生产前测试:build 后检查日志是否有 PPR 标识。
常见坑点:
未启用 experimental.ppr → 全部静态或动态,无混合。
过多动态孔 → 失去 PPR 优势。
忘记 Suspense → 动态部分白屏。
Takeaway:PPR 是 Next.js 15 的杀手级特性,代表未来方向:静态性能 + 动态实时,无需妥协。
关键概念:Route Handlers 是 Next.js 的内置 API 路由(app/api/route.ts),支持 GET/POST/PUT/DELETE 等方法,运行在 Edge 或 Node.js 环境。取代传统的 /pages/api 目录,更轻量、更集成。
核心技术点:
重要代码片段:
app/api/hello/route.ts(基本 GET):
xxxxxxxxxx101import { NextResponse } from 'next/server';23export async function GET(request: Request) {4 return NextResponse.json({ message: 'Hello from Route Handler!' });5}67export async function POST(request: Request) {8 const body = await request.json();9 return NextResponse.json({ received: body });10}访问 headers/cookies/searchParams:
xxxxxxxxxx71export async function GET(request: NextRequest) {2 const userAgent = request.headers.get('user-agent');3 const theme = request.cookies.get('theme')?.value;4 const query = request.nextUrl.searchParams.get('q');56 return NextResponse.json({ userAgent, theme, query });7}最佳实践:
用于轻量 API 或 webhook。
复杂逻辑放 Server Actions 或 DAL。
避免在 Route Handler 中放 DB 查询(优先 Server Components)。
常见坑点:
忘记 export 方法名 → 405 Method Not Allowed。
使用 Response 而非 NextResponse → 丢失 Edge 支持。
Takeaway:Route Handlers 是现代 API 路由方式,轻量且与 App Router 深度集成。
关键概念:Route Handler 中使用 Drizzle 查询数据库,返回 JSON(常用于分页、过滤、内部 API)。
核心技术点:
重要代码片段:
app/api/posts/route.ts(带分页 + 过滤):
xxxxxxxxxx381import { db } from '@/db';2import { posts, users } from '@/db/schema';3import { desc, eq, sql } from 'drizzle-orm';4import { NextResponse } from 'next/server';56export async function GET(request: NextRequest) {7 const searchParams = request.nextUrl.searchParams;8 const page = Number(searchParams.get('page')) || 1;9 const limit = Number(searchParams.get('limit')) || 10;10 const search = searchParams.get('q');1112 const offset = (page - 1) * limit;1314 const whereClause = search15 ? sql`${posts.title} ILIKE ${`%${search}%`}`16 : undefined;1718 const result = await db.query.posts.findMany({19 where: whereClause,20 with: {21 author: { columns: { id: true, name: true } },22 },23 orderBy: desc(posts.createdAt),24 limit,25 offset,26 });2728 const total = await db.select({ count: sql<number>`count(*)` })29 .from(posts)30 .where(whereClause);3132 return NextResponse.json({33 posts: result,34 total: Number(total[0].count),35 page,36 limit,37 });38}最佳实践:
用 searchParams 处理分页/搜索。
验证输入防 SQL 注入(Drizzle 自动防,但仍需 sanitization)。
返回结构化 JSON(posts + meta)。
常见坑点:
忘记 await db 查询 → Promise 返回。
无错误处理 → 500 错误。
Takeaway:Route Handler 适合暴露 DB 数据给前端 fetch 或第三方,简单高效。
这一步结束!第四步聚焦渲染优化(Streaming + PPR)和 API 方式(Route Handlers),是性能 & 数据交互的关键。
准备好第五步了吗?回复“第五步”或“第5步”继续~ 🚀
第五步笔记
涵盖章节:Route Handlers Example (Webhooks) + Server & Client Components (Load More Posts Button) + Server Functions Overview
关键概念:Route Handlers 常用于处理外部 webhook(如 Stripe 支付成功、GitHub 事件、邮件服务回调)。视频演示 Stripe webhook:验证签名 → 处理事件 → 更新数据库 → 返回 200 响应(必须 200 否则 Stripe 重试)。
核心技术点:
重要代码片段:
app/api/webhooks/stripe/route.ts(完整 webhook handler):
xxxxxxxxxx571import { NextRequest, NextResponse } from 'next/server';2import Stripe from 'stripe';3import { db } from '@/db';4import { users } from '@/db/schema';5import { eq } from 'drizzle-orm';67const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {8 apiVersion: '2024-06-20',9});1011const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;1213export async function POST(req: NextRequest) {14 const body = await req.text(); // 必须 raw body15 const signature = req.headers.get('stripe-signature')!;1617 let event: Stripe.Event;1819 try {20 event = stripe.webhooks.constructEvent(body, signature, webhookSecret);21 } catch (err: any) {22 console.error('Webhook signature verification failed:', err.message);23 return NextResponse.json({ error: 'Webhook Error' }, { status: 400 });24 }2526 // 处理不同事件27 switch (event.type) {28 case 'payment_intent.succeeded':29 const paymentIntent = event.data.object as Stripe.PaymentIntent;30 const userId = paymentIntent.metadata.userId;3132 // 更新用户订阅状态33 await db.update(users)34 .set({ subscriptionStatus: 'active' })35 .where(eq(users.id, userId));3637 console.log(`Payment succeeded for user ${userId}`);38 break;3940 case 'invoice.payment_failed':41 // 处理失败逻辑42 break;4344 default:45 console.log(`Unhandled event type ${event.type}`);46 }4748 // 必须返回 20049 return NextResponse.json({ received: true });50}5152// 禁用 body parser(必须 raw body)53export const config = {54 api: {55 bodyParser: false,56 },57};最佳实践:
用 raw body(req.text())而非 req.json()。
立即返回 200,避免 Stripe 重试。
事件处理异步化(queue 或 background job)。
常见坑点:
用 req.json() → 签名验证失败(body 已解析)。
未返回 200 → Stripe 无限重试。
暴露 webhookSecret 在代码中。
Takeaway:Route Handlers 是 webhook 最佳入口,验证 + 快速响应是关键。
关键概念:展示 Server Components(默认、无 JS)和 Client Components("use client"、状态/交互)的混合使用。实现“Load More”按钮:Client 管理状态 + Server Action 加载下一页数据。
核心技术点:
重要代码片段:
Client 组件(LoadMoreButton.tsx):
xxxxxxxxxx391'use client';23import { useState, useTransition } from 'react';4import { loadMorePosts } from '@/actions/posts';56type Props = { initialPosts: any[]; initialPage: number };78export default function LoadMoreButton({ initialPosts, initialPage }: Props) {9 const [posts, setPosts] = useState(initialPosts);10 const [page, setPage] = useState(initialPage);11 const [isPending, startTransition] = useTransition();12 const [hasMore, setHasMore] = useState(true);1314 const handleLoadMore = async () => {15 startTransition(async () => {16 const newPosts = await loadMorePosts(page + 1);17 if (newPosts.length === 0) {18 setHasMore(false);19 } else {20 setPosts(prev => [prev, newPosts]);21 setPage(page + 1);22 }23 });24 };2526 return (27 <div className="mt-8 text-center">28 {hasMore && (29 <button30 onClick={handleLoadMore}31 disabled={isPending}32 className="px-6 py-3 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"33 >34 {isPending ? 'Loading...' : 'Load More Posts'}35 </button>36 )}37 </div>38 );39}Server Action(actions/posts.ts):
xxxxxxxxxx171'use server';23import { db } from '@/db';4import { posts } from '@/db/schema';5import { desc } from 'drizzle-orm';67export async function loadMorePosts(page: number) {8 const limit = 10;9 const offset = (page - 1) * limit;1011 return db.query.posts.findMany({12 orderBy: desc(posts.createdAt),13 limit,14 offset,15 with: { author: true },16 });17}最佳实践:
用 useTransition 防止 UI 阻塞。
Server Action 返回纯数据(JSON serializable)。
初始数据从 Server Component 传入。
常见坑点:
Client 中直接 DB 查询 → 错误。
无 transition → 按钮点击卡顿。
Takeaway:Server + Client 混合是 Next.js 15 核心模式,实现“Load More”无需传统 API。
关键概念:Server Functions(Server Actions)是 'use server' 标记的异步函数,可直接在 form action 或 Client 组件调用。取代传统 API 路由,更安全(自动 CSRF 保护)、更简单、无需额外 endpoint。
核心技术点:
重要代码片段:
actions.ts(文件级 Server Actions):
xxxxxxxxxx301'use server';23import { db } from '@/db';4import { posts } from '@/db/schema';5import { revalidatePath } from 'next/cache';67export async function createPost(formData: FormData) {8 const title = formData.get('title') as string;9 const content = formData.get('content') as string;1011 // 权限检查(后续章节)12 // const session = await auth();1314 await db.insert(posts).values({15 title,16 content,17 authorId: 'some-user-id', // 从 session18 });1920 revalidatePath('/blog');2122 return { success: true };23}2425// 函数级标记示例26export async function deletePost(id: string) {27 'use server';28 await db.delete(posts).where(eq(posts.id, id));29 revalidatePath('/blog');30}表单使用:
xxxxxxxxxx51<form action={createPost}>2 <input name="title" />3 <textarea name="content" />4 <button type="submit">Create Post</button>5</form>最佳实践:
文件级 'use server' 标记整个 actions.ts。
用 Zod 验证 formData。
结合 revalidatePath 失效缓存。
常见坑点:
忘记 'use server' → 函数在 Client 执行 → 泄露秘密。
返回非 JSON → 序列化错误。
Takeaway:Server Actions 是 Next.js 15 mutation 首选方式,简单、安全、渐进增强。
这一步结束!第五步聚焦 webhook 处理、Server/Client 混合交互和 Server Actions 基础——Next.js 15 现代交互模式的精华。
准备好第六步了吗?回复“第六步”或“第6步”继续~ 🚀
第六步笔记
涵盖章节:Edit Post Server Action + useFormStatus & useActionState + Handling Errors in Form Actions + Toggle Post Like Action + Toggle Post Like Action (Cont.)
关键概念:Server Actions 是 Next.js 15 推荐的 mutation 方式,直接在 form action 中调用 'use server' 函数。视频演示编辑帖子:从表单提交 → 绑定 ID → 更新 DB → revalidate 缓存 → 返回成功/错误。
核心技术点:
重要代码片段:
actions/posts.ts(Server Action 文件):
xxxxxxxxxx371'use server';23import { db } from '@/db';4import { posts } from '@/db/schema';5import { eq } from 'drizzle-orm';6import { revalidatePath } from 'next/cache';7import { z } from 'zod';89const editPostSchema = z.object({10 title: z.string().min(3).max(100),11 content: z.string().min(10),12});1314export async function editPostAction(formData: FormData) {15 const id = formData.get('id') as string; // 从 bind 传入16 const title = formData.get('title') as string;17 const content = formData.get('content') as string;1819 // 验证20 const validated = editPostSchema.safeParse({ title, content });21 if (!validated.success) {22 return { error: validated.error.format() };23 }2425 // 权限检查(简化,实际用 session)26 // const session = await auth();27 // if (!session?.user) throw new Error('Unauthorized');2829 await db.update(posts)30 .set({ title: validated.data.title, content: validated.data.content })31 .where(eq(posts.id, id));3233 revalidatePath('/blog');34 revalidatePath(`/posts/${id}`);3536 return { success: true };37}使用 bind 安全传 ID(在 Server Component 中):
xxxxxxxxxx91// app/posts/[id]/edit/page.tsx2const editPostBound = editPostAction.bind(null, id); // 安全绑定 ID34<form action={editPostBound}>5 <input type="hidden" name="id" value={id} /> {/* 实际可省略 */}6 <input name="title" defaultValue={post.title} />7 <textarea name="content" defaultValue={post.content} />8 <button type="submit">Update Post</button>9</form>最佳实践:
bind() 传递动态参数,避免 hidden input 暴露。
Zod 验证 + 友好错误返回。
revalidatePath 保持数据一致。
常见坑点:
忘记 bind → ID 未传 → 更新失败。
无验证 → 恶意输入。
Takeaway:Server Actions + bind 是编辑/CRUD 的最简安全方式。
关键概念:useFormStatus(旧) + useActionState(Next.js 15 推荐)提供表单状态(pending、data、error)。useActionState 整合 pending + 返回值 + 重置。
核心技术点:
重要代码片段:
编辑表单(useActionState):
xxxxxxxxxx401'use client';23import { useActionState } from 'react';4import { editPostAction } from '@/actions/posts';56export default function EditPostForm({ id, initialTitle, initialContent }) {7 const [state, formAction, isPending] = useActionState(editPostAction, null);89 return (10 <form action={formAction} className="space-y-6">11 <input type="hidden" name="id" value={id} />1213 <div>14 <label>Title</label>15 <input name="title" defaultValue={initialTitle} className="border p-2 w-full" />16 </div>1718 <div>19 <label>Content</label>20 <textarea name="content" defaultValue={initialContent} className="border p-2 w-full h-40" />21 </div>2223 <button24 type="submit"25 disabled={isPending}26 className="px-6 py-2 bg-blue-600 text-white rounded disabled:opacity-50"27 >28 {isPending ? 'Updating...' : 'Update Post'}29 </button>3031 {state?.error && (32 <div className="text-red-600">33 {state.error.title?._errors?.[0] || state.error.content?._errors?.[0] || 'Error occurred'}34 </div>35 )}3637 {state?.success && <div className="text-green-600">Post updated successfully!</div>}38 </form>39 );40}最佳实践:
按钮禁用 + loading 文本。
显示字段级错误(Zod format)。
常见坑点:
忘记 disabled → 双提交。
无 error 处理 → 用户困惑。
Takeaway:useActionState 让表单交互更现代、响应式。
关键概念:Server Action 中 try-catch + 返回 error 对象,Client 用 state.error 显示。支持字段级错误(Zod)。
核心技术点:
重要代码片段:
Action 中返回错误:
xxxxxxxxxx111export async function editPostAction(formData: FormData) {2 try {3 // 验证 + 更新...4 return { success: true };5 } catch (err) {6 if (err instanceof z.ZodError) {7 return { error: err.format() };8 }9 return { error: { _errors: ['Something went wrong'] } };10 }11}Client 显示:
xxxxxxxxxx11{state?.error?._errors?.map((e, i) => <p key={i} className="text-red-600">{e}</p>)}最佳实践:
Zod + format() → 字段级错误。
全局 catch 处理意外错误。
常见坑点:
只抛 error → 页面崩溃。
返回字符串而非对象 → Client 解析困难。
Takeaway:错误处理让表单更友好,避免用户挫败。
关键概念:点赞/取消点赞的 Server Action:检查是否已赞 → insert/delete → 返回新状态。
核心技术点:
重要代码片段:
actions/likes.ts:
xxxxxxxxxx251'use server';23import { db } from '@/db';4import { postLikes } from '@/db/schema';5import { eq, and } from 'drizzle-orm';6import { auth } from '@/auth'; // BetterAuth78export async function toggleLikeAction(postId: string) {9 const session = await auth();10 if (!session?.user?.id) throw new Error('Unauthorized');1112 const userId = session.user.id;1314 const existing = await db.query.postLikes.findFirst({15 where: and(eq(postLikes.postId, postId), eq(postLikes.userId, userId)),16 });1718 if (existing) {19 await db.delete(postLikes).where(eq(postLikes.id, existing.id));20 return { liked: false };21 } else {22 await db.insert(postLikes).values({ postId, userId });23 return { liked: true };24 }25}最佳实践:
权限检查。
返回 liked 状态供 Client 更新 UI。
常见坑点:
无 auth → 匿名点赞。
竞争条件 → 双点赞。
Takeaway:点赞是典型 mutation,Server Action 完美处理。
关键概念:扩展 toggleLike:返回最新 count(点赞数),供 Client 即时显示。结合 revalidateTag 失效缓存。
核心技术点:
重要代码片段(完整版):
xxxxxxxxxx251 export async function toggleLikeAction(postId: string) {2 const session = await auth();3 if (!session?.user?.id) throw new Error('Unauthorized');45 const userId = session.user.id;67 const existing = await db.query.postLikes.findFirst({8 where: and(eq(postLikes.postId, postId), eq(postLikes.userId, userId)),9 });1011 if (existing) {12 await db.delete(postLikes).where(eq(postLikes.id, existing.id));13 } else {14 await db.insert(postLikes).values({ postId, userId });15 }1617 // 获取最新 count18 const [{ count }] = await db.select({ count: sql<number>`count(*)` })19 .from(postLikes)20 .where(eq(postLikes.postId, postId));2122 revalidateTag(`post-likes-${postId}`); // 失效特定 post 的 likes2324 return { liked: !existing, count: Number(count) };25 }Client 使用(结合 useOptimistic,后续章节):
xxxxxxxxxx21const newState = await toggleLikeAction(postId);2setLikes(newState.count);最佳实践:
返回 count 供 Client 更新。
用 tag 失效相关缓存。
常见坑点:
忘记 revalidate → 列表点赞数不更新。
count 类型 string → Number() 转换。
Takeaway:完整 toggle 逻辑为 Optimistic UI 铺路,实现实时点赞。
这一步结束!第六步是 Server Actions 的核心实战部分,涵盖编辑、状态管理、错误处理和点赞交互——Next.js 15 mutation 的精华。
准备好第七步了吗?回复“第七步”或“第7步”继续~ 🚀
第七步笔记
涵盖章节:Optimistic UI Updates with useOptimistic + Error Handling + Middleware + Authentication: Installing BetterAuth + GitHub Login
关键概念:useOptimistic 是 React 19 / Next.js 15 的实验钩子,用于实现“乐观更新”(Optimistic UI):立即更新 UI 反馈(如点赞按钮变色/数字 +1),然后异步提交服务器,失败时回滚。结合 useTransition 防止阻塞。
核心技术点:
重要代码片段:
Client 组件(PostLikeButton.tsx):
xxxxxxxxxx431'use client';23import { useOptimistic, useTransition } from 'react';4import { toggleLikeAction } from '@/actions/likes';56type Props = { postId: string; initialLikes: number; initialLiked: boolean };78export default function PostLikeButton({ postId, initialLikes, initialLiked }: Props) {9 const [isPending, startTransition] = useTransition();1011 const [optimisticState, addOptimistic] = useOptimistic(12 { likes: initialLikes, liked: initialLiked },13 (current, { liked, delta }) => ({14 likes: current.likes + delta,15 liked,16 })17 );1819 const handleToggle = () => {20 const newLiked = !optimisticState.liked;21 const delta = newLiked ? 1 : -1;2223 startTransition(async () => {24 try {25 addOptimistic({ liked: newLiked, delta }); // 立即更新 UI26 await toggleLikeAction(postId); // 服务器提交27 } catch (err) {28 // 失败自动回滚(useOptimistic 内置)29 console.error('Like failed:', err);30 }31 });32 };3334 return (35 <button36 onClick={handleToggle}37 disabled={isPending}38 className={`px-4 py-2 rounded ${optimisticState.liked ? 'bg-red-600 text-white' : 'bg-gray-200'}`}39 >40 {optimisticState.liked ? 'Unlike' : 'Like'} ({optimisticState.likes})41 </button>42 );43}最佳实践:
updateFn 必须是纯函数,返回新状态。
用 startTransition 包裹 action 调用。
失败自动回滚(无需手动)。
常见坑点:
忘记 transition → UI 阻塞。
updateFn 返回旧状态 → 无更新。
Takeaway:useOptimistic 让交互(如点赞)即时响应,大幅提升用户体验,是现代 UI 标配。
关键概念:Next.js 提供多级错误处理:error.tsx(边界组件)、notFound()、redirect()、全局 error.tsx。Server Actions 中 try-catch 返回错误,Client 显示。
核心技术点:
重要代码片段:
app/posts/[id]/error.tsx(边界错误):
xxxxxxxxxx281'use client';23import { useEffect } from 'react';45export default function Error({6 error,7 reset,8}: {9 error: Error & { digest?: string };10 reset: () => void;11}) {12 useEffect(() => {13 console.error(error);14 }, [error]);1516 return (17 <div className="text-center p-10">18 <h2 className="text-2xl font-bold text-red-600">Something went wrong!</h2>19 <p className="mt-4 text-gray-600">{error.message || 'An unexpected error occurred.'}</p>20 <button21 onClick={reset}22 className="mt-6 px-6 py-3 bg-blue-600 text-white rounded hover:bg-blue-700"23 >24 Try again25 </button>26 </div>27 );28}Server Action 错误处理:
xxxxxxxxxx51export async function getPost(id: string) {2 const post = await db.query.posts.findFirst({ where: eq(posts.id, id) });3 if (!post) notFound(); // 自动 4044 return post;5}最佳实践:
每个页面/段落用 error.tsx。
全局 error.tsx 作为兜底。
Server Action 返回 { error } 而非抛出(避免崩溃)。
常见坑点:
无 error.tsx → 整个页面白屏。
抛出未捕获 → 500 错误。
Takeaway:多级错误处理让应用更鲁棒,用户体验友好。
关键概念:middleware.ts 运行在 Edge,拦截所有请求,进行认证、重定向、header 修改等。常用于保护路由、国际化、A/B 测试。
核心技术点:
重要代码片段:
middleware.ts(保护 dashboard):
xxxxxxxxxx241import { NextRequest, NextResponse } from 'next/server';2import { auth } from '@/auth'; // BetterAuth34export async function middleware(request: NextRequest) {5 const session = await auth();67 // 保护 /dashboard 和子路径8 if (request.nextUrl.pathname.startsWith('/dashboard')) {9 if (!session?.user) {10 const url = new URL('/login', request.url);11 url.searchParams.set('redirectTo', request.nextUrl.pathname);12 return NextResponse.redirect(url);13 }14 }1516 return NextResponse.next();17}1819export const config = {20 matcher: [21 '/dashboard/:path*', // 匹配 /dashboard 和子路径22 '/profile/:path*',23 ],24};最佳实践:
只匹配必要路径(matcher 性能)。
redirectTo 参数保存原路径。
Edge 运行 → 速度快。
常见坑点:
matcher 错 → 无限重定向。
忘记 await auth() → session null。
Takeaway:Middleware 是全局守门员,保护私有路由最简单方式。
关键概念:BetterAuth 是简单、现代的认证库,支持 Drizzle adapter、OAuth、魔法链接等。视频演示安装 + 配置 + Drizzle 集成。
核心技术点:
重要代码片段:
lib/auth.ts:
xxxxxxxxxx251import { betterAuth } from 'better-auth';2import { drizzleAdapter } from 'better-auth/adapters/drizzle';3import { db } from '@/db';4import { accounts, sessions, users } from '@/db/schema';56export const auth = betterAuth({7 database: drizzleAdapter(db, {8 provider: 'pg',9 usersTable: users,10 accountsTable: accounts,11 sessionsTable: sessions,12 }),1314 socialProviders: {15 github: {16 clientId: process.env.GITHUB_CLIENT_ID!,17 clientSecret: process.env.GITHUB_CLIENT_SECRET!,18 },19 },2021 // 可选:email/password 等22 emailAndPassword: {23 enabled: true,24 },25});最佳实践:
用 Drizzle adapter 复用现有 schema。
环境变量存储 secret。
常见坑点:
schema 表名不匹配 → 迁移失败。
忘记 env → 认证失败。
Takeaway:BetterAuth 简化认证,比 NextAuth 更轻量。
关键概念:配置 GitHub OAuth provider,实现一键登录 + session 创建。回调处理 + redirect。
核心技术点:
重要代码片段:
在 auth 配置中添加 provider(见上)。
登录按钮(Client):
xxxxxxxxxx141'use client';23import { signIn } from 'better-auth/react';45export default function LoginButton() {6 return (7 <button8 onClick={() => signIn('github', { redirectTo: '/dashboard' })}9 className="px-6 py-3 bg-black text-white rounded hover:bg-gray-800"10 >11 Login with GitHub12 </button>13 );14}最佳实践:
redirectTo 保存原路径。
生产环境用 HTTPS。
常见坑点:
GitHub App callback URL 未配置 → 回调失败。
忘记 signIn 导入。
Takeaway:GitHub OAuth 是最快认证方式,BetterAuth 让集成极简。
这一步结束!第七步聚焦 Optimistic UI、错误处理、全局保护和认证集成——Next.js 15 安全 & 交互的精髓。
准备好第八步(最后一步)了吗?回复“第八步”或“第8步”继续~ 🚀
第八步笔记
涵盖章节:Protecting Routes + redirectTo Search Parameter + Displaying User Info & Logging Out + Optimistic Redirects with Middleware + Protecting Server Actions + Tainting Objects & Values + Data Access Layers + Outro
关键概念:保护私有路由(如 /dashboard、/profile),未登录用户自动重定向到登录页。结合 Middleware 或 Server Component 检查 session。
核心技术点:
重要代码片段:
middleware.ts(推荐方式):
xxxxxxxxxx221import { NextRequest, NextResponse } from 'next/server';2import { auth } from '@/auth';34export async function middleware(request: NextRequest) {5 const session = await auth();67 const protectedPaths = ['/dashboard', '/profile', '/settings'];89 if (protectedPaths.some(path => request.nextUrl.pathname.startsWith(path))) {10 if (!session?.user) {11 const url = new URL('/login', request.url);12 url.searchParams.set('redirectTo', request.nextUrl.pathname);13 return NextResponse.redirect(url);14 }15 }1617 return NextResponse.next();18}1920export const config = {21 matcher: ['/dashboard/:path*', '/profile/:path*', '/settings/:path*'],22};最佳实践:
用 matcher 只拦截必要路径,避免全局影响。
结合 redirectTo 参数(下一章)。
常见坑点:
matcher 漏路径 → 未保护。
session 未 await → null 判断失败。
Takeaway:Middleware 是最干净的路由保护方式,集中管理。
关键概念:登录后自动跳转回用户原本想访问的页面。使用 ?redirectTo=... 参数保存原路径。
核心技术点:
重要代码片段:
登录页(app/login/page.tsx):
xxxxxxxxxx161import { redirect } from 'next/navigation';2import { signIn } from 'better-auth/react';34export default function LoginPage({ searchParams }: { searchParams: Promise<{ redirectTo?: string }> }) {5 const handleGitHubLogin = async () => {6 const params = await searchParams;7 const redirectTo = params.redirectTo || '/dashboard';8 await signIn('github', { redirectTo });9 };1011 return (12 <div>13 <button onClick={handleGitHubLogin}>Login with GitHub</button>14 </div>15 );16}Middleware 中设置(上一章已示):
xxxxxxxxxx11url.searchParams.set('redirectTo', request.nextUrl.pathname);最佳实践:
始终提供 fallback(如 '/dashboard')。
encodeURIComponent 处理特殊字符。
常见坑点:
无 redirectTo → 登录后总去首页。
未 encode → URL 解析错误。
Takeaway:redirectTo 参数极大提升用户体验,避免“登录后迷路”。
关键概念:从 session 获取用户信息显示在 UI(如头像、名称、下拉菜单)。实现登出功能(signOut)。
核心技术点:
重要代码片段:
Client 组件(Header.tsx):
xxxxxxxxxx311'use client';23import { useSession, signOut } from 'better-auth/react';45export default function Header() {6 const { data: session } = useSession();78 return (9 <header className="p-4 bg-gray-800 text-white">10 <div className="container mx-auto flex justify-between">11 <h1>Blog App</h1>12 {session?.user ? (13 <div className="flex items-center gap-4">14 <span>Welcome, {session.user.name}</span>15 <img src={session.user.image} alt="avatar" className="w-8 h-8 rounded-full" />16 <button17 onClick={() => signOut({ redirectTo: '/' })}18 className="px-4 py-2 bg-red-600 rounded hover:bg-red-700"19 >20 Logout21 </button>22 </div>23 ) : (24 <Link href="/login" className="px-4 py-2 bg-blue-600 rounded hover:bg-blue-700">25 Login26 </Link>27 )}28 </div>29 </header>30 );31}最佳实践:
用 useSession() 响应式更新。
signOut 带 redirectTo。
常见坑点:
直接 session.user → 未加载时 null。
登出后不重定向 → 卡在私有页。
Takeaway:session 驱动 UI 状态,实现登录/登出无缝切换。
关键概念:登录过程中“乐观重定向”:用户点击登录 → 立即重定向到目标页 → 登录成功后真正跳转。结合 Middleware 提前检查。
核心技术点:
重要代码片段:
Middleware 扩展:
xxxxxxxxxx101if (request.nextUrl.pathname.startsWith('/dashboard') && !session?.user) {2 if (request.cookies.has('logging_in')) {3 // 乐观:已点击登录,等待回调4 return NextResponse.next(); // 继续访问,回调后会成功5 }6 // 正常重定向7 const url = new URL('/login', request.url);8 url.searchParams.set('redirectTo', request.nextUrl.pathname);9 return NextResponse.redirect(url);10}最佳实践:
用 cookie 标记 'logging_in'。
回调成功后清 cookie。
常见坑点:
未清 cookie → 永久乐观。
Takeaway:乐观重定向减少等待,提升流畅感。
关键概念:Server Actions 运行在服务器,必须内部检查 session,防止未授权执行(如删除他人帖子)。
核心技术点:
重要代码片段:
actions/posts.ts:
xxxxxxxxxx171'use server';23import { auth } from '@/auth';45export async function deletePost(id: string) {6 const session = await auth();7 if (!session?.user) throw new Error('Unauthorized');89 // 进一步检查是否作者10 const post = await db.query.posts.findFirst({ where: eq(posts.id, id) });11 if (post?.authorId !== session.user.id) {12 throw new Error('Not your post');13 }1415 await db.delete(posts).where(eq(posts.id, id));16 revalidatePath('/blog');17}最佳实践:
每个 mutation Action 都检查 auth。
结合 ownerId 字段防越权。
常见坑点:
漏检查 → 安全漏洞。
Takeaway:Server Actions 安全依赖内部校验。
Tainting Objects & Values
关键技术点:React 19(实验特性)引入的 taint API,用于防止敏感数据(如用户完整对象、token、密码、session 密钥)意外从 Server Component 泄露到 Client Component。
Next.js 15+ 支持通过 next.config.js 启用(experimental: { taint: true })。
experimental_taintObjectReference(message, object):污染整个对象引用,防止该实例直接传递到 Client。 experimental_taintUniqueValue(message, lifetime, value):污染特定唯一值(如 token),防止泄露。xxxxxxxxxx81/** @type {import('next').NextConfig} */2const nextConfig = {3 experimental: {4 taint: true,5 },6};78export default nextConfig;重要代码片段(视频中最典型用法:污染 user 对象):
xxxxxxxxxx221// lib/data.ts 或 actions.ts(Server-only 文件)2import { experimental_taintObjectReference, experimental_taintUniqueValue } from 'react';34export async function getUser(id: string) {5 const user = await db.query.users.findFirst({ where: eq(users.id, id) });67 // 污染整个 user 对象引用8 experimental_taintObjectReference(9 'Do not pass the entire user object to the client. Instead, pick off the specific properties you need for this use case.',10 user11 );1213 // 额外污染敏感字段(如 token 或 password)14 experimental_taintUniqueValue(15 'Do not pass authentication tokens or passwords to the client.',16 user, // lifetime reference (对象)17 user.token // 具体要污染的值18 );1920 // 返回时,如果 Client 试图接收整个 user,会在开发时抛出自定义错误21 return user;22}常见场景:
process.env:experimental_taintObjectReference('Do not pass env vars to client', process.env);最佳实践:
import 'server-only'; 防止文件被 Client 导入。常见坑点:
experimental.taint: true → 无效果。{ ...user }) → 新对象未污染。user.token.toUpperCase())未自动污染 → 需手动再 taint。Takeaway:高级防御机制,防人为失误泄露敏感数据。Next.js 15 + React 19 标配安全实践。
关键技术点:抽象数据访问层(Data Access Layer),集中管理所有数据库查询/操作,提供复用、安全过滤、类型安全接口。
核心概念:避免在 Component/Action 中散落 Drizzle 查询;DAL 作为中间层,统一处理权限、过滤、错误、缓存等。
视频中强调:DAL 使代码可维护、可测试、可扩展,尤其在团队协作/大型项目中。
重要代码片段(视频典型 DAL 结构):
lib/db/ 或 src/data/ xxxxxxxxxx721// lib/data/posts.ts2'use server'; // 可选,确保 server-only3import { db } from '@/db/drizzle'; // 全局 db 实例4import { posts, users, postLikes } from '@/db/schema';5import { eq, desc, and } from 'drizzle-orm';6import { notFound } from 'next/navigation';7import { auth } from '@/auth'; // BetterAuth 或其他 auth89// 获取所有帖子(公开)10export async function getPosts(limit = 10, offset = 0) {11 return db.query.posts.findMany({12 with: {13 author: { columns: { id: true, name: true } },14 likes: true,15 },16 orderBy: desc(posts.createdAt),17 limit,18 offset,19 });20}2122// 获取单帖(带权限检查)23export async function getPostById(id: string) {24 const post = await db.query.posts.findFirst({25 where: eq(posts.id, id),26 with: {27 author: true,28 likes: true,29 },30 });3132 if (!post) notFound();33 return post;34}3536// 创建帖子(Server Action 专用)37export async function createPost(data: { title: string; content: string }) {38 const session = await auth();39 if (!session?.user) throw new Error('Unauthorized');4041 const [newPost] = await db.insert(posts).values({42 title: data.title,43 content: data.content,44 authorId: session.user.id,45 }).returning();4647 return newPost;48}4950// 点赞 toggle(乐观更新准备)51export async function toggleLike(postId: string) {52 const session = await auth();53 if (!session?.user) throw new Error('Unauthorized');5455 const existing = await db.query.postLikes.findFirst({56 where: and(57 eq(postLikes.postId, postId),58 eq(postLikes.userId, session.user.id!)59 ),60 });6162 if (existing) {63 await db.delete(postLikes).where(eq(postLikes.id, existing.id));64 return { liked: false };65 } else {66 await db.insert(postLikes).values({67 postId,68 userId: session.user.id!,69 });70 return { liked: true };71 }72}最佳实践:
import 'server-only'; 防止误导入 Client。常见坑点:
Takeaway:DAL 是大型 Next.js 项目可维护性关键,集中管理 DB 逻辑,避免代码散乱,提升安全 & 测试性。
这些是视频中对应章节的核心实现(基于 Ali Alaa 的教学风格:逐步抽象 + 安全优先)。如果想更细(如完整 toggleLike action 与 Optimistic 结合),或需要其他章节扩展,再告诉我!🚀
关键概念:课程总结:从 setup 到完整博客(CRUD、auth、优化)。部署 Vercel,扩展建议(搜索、评论、SEO)。
代码片段:vercel deploy。
Takeaway:学完可独立建生产级应用,继续实践!
课程全部结束!恭喜完成所有 8 步~ 如果想回顾某步、扩展代码、或问相关问题,随时说!🚀